package org.fujene.memoryblock;

import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import org.fujene.structure.LookupPointer;
import org.fujene.toolbox.ByteConverter;
import org.msgpack.MessagePackObject;
import org.msgpack.object.ArrayType;
import org.msgpack.object.IntegerType;
import org.msgpack.object.RawType;

public abstract class MemoryBlock{
	private int ID;
	private int type;
	protected int headerSize;
	protected int bodySize;
	// private byte[] body;
	private ByteBuffer body;
	protected int[] option;
	private int cluster;
	protected int optionSize;
	protected volatile int bodyVacancy;
	protected double score;
	private boolean isAboutToFlush;

	private static final byte[] HEADER = {0x46, 0x61, 0x52, 50};
	protected static final int HEADERSIZE = 64;
	protected static final int PTR = LookupPointer.PTR; // 6
	protected static final int PTRSHORT = LookupPointer.PTRSHORT; // 4
	protected static final int LONG = 8;
	protected static final int INT = 4;
	protected static final int STRLEN = 2;
	protected static final int DOCFREQ = 3;
	protected static final int TERMFREQ = 2;
	protected static final int IDDIFF = 5;
	protected static final int NEWEST = 2;
	protected static final int FLAGLEN = 2;
	protected static final int TYPE = 1;

	public MemoryBlock(ByteBuffer body, int bodySize, int ID, int type, int cluster){
		this.ID = ID;
		this.bodySize = bodySize;
		this.type = type;
		this.body = body;
		this.bodySize = bodySize;
		this.bodyVacancy = this.bodySize;
		this.cluster = cluster;
		this.option = new int[8];
		this.isAboutToFlush = false;
	}

	public MemoryBlock(ByteBuffer existingBody){
		this.body = existingBody;
		this.option = new int[8];
		this.readHeader();
	}

	public int getID(){
		return ID;
	}

	public void setID(int newID){
		this.ID = newID;
	}

	public int getBodySize(){
		return this.bodySize;
	}

	public abstract int getOption(int num);

	public abstract int getOptionSize();

	public abstract byte[] getOptionByteStream();

	public abstract void setOption(int option, int value);

	// read with copy
	public synchronized byte[] read(int from, int length){
		if(from + length > bodySize || length < 0)
			throw new IndexOutOfBoundsException("Out of bound from " + from + ", length: " + length);

		byte[] readb = new byte[length];
		synchronized(body){
			body.position(from + HEADERSIZE);
			body.get(readb);
		}

		return readb;
		// return Arrays.copyOfRange(body, from, from + length);
	}

	protected void move(int from, int length, int offset){
		if(offset == 0) return; // Do nothing
		if(from + offset < 0 || from + length + offset >= bodySize)
			throw new IndexOutOfBoundsException(String.format(
				"Out of bound. From: %d, Length: %d, Offset: %d, Body-size: %d", from, length, offset,
				bodySize));
		byte[] readb = new byte[length];
		synchronized(body){
			body.position(from + HEADERSIZE);
			body.get(readb);
			body.position(from + offset + HEADERSIZE);
			body.put(readb);
			// if(offset > 0){
			// // move backward from the last byte
			// for(int i = from + length - 1; i >= from; i--)
			// body[i + offset] = body[i];
			// }else{
			// // move forward from the first byte
			// for(int i = from; i < from + length - 1; i++)
			// body[i + offset] = body[i];
			// }
		}
	}

	public int getType(){
		return type;
	}

	public void store(byte[] values, int from){
		if(from + values.length > bodySize) return;

		synchronized(body){
			body.position(from + HEADERSIZE);
			try{
				body.put(values);
			}catch(BufferOverflowException e){
				System.out.printf("Error: BufferOverFlowException position: %d, length: %d, limit: %d%n",
					body.position(), values.length, body.limit());
				e.printStackTrace();
			}
			// for(int i = 0; i < values.length; i++)
			// body[from + i] = values[i];
		}
	}

	public void setBody(byte[] values, int vfrom, int vlen, int bfrom){
		if(bfrom + vlen > bodySize) return;

		synchronized(body){
			body.position(bfrom + HEADERSIZE);
			body.put(values, vfrom, vlen);
			// for(int i = 0; i < vlen; i++)
			// body[bfrom + i] = values[vfrom + i];
		}
	}

	protected int getInt(int ptr, int len){
		int temp = 0;

		byte[] intb = read(ptr, len);
		for(int i = 0; i < len; i++){
			// Big endian
			temp <<= 8;
			temp += (intb[i] & 0xff);
		}
		// Make complement
		if(temp >= 0 && ((temp & (1 << (len * 8 - 1))) != 0)) temp -= (1 << (len * 8));

		return temp;
	}

	protected long getLong(int ptr, int len){
		long temp = 0;

		byte[] longb = read(ptr, len);
		for(int i = 0; i < len; i++){
			// Big endian
			temp <<= 8;
			temp += (longb[i] & 0xff);
		}
		// Make complement
		if(temp >= 0 && ((temp & (1L << (len * 8 - 1))) != 0)) temp -= (1L << (len * 8));

		return temp;
	}

	protected LookupPointer getPointer(int ptr, int len){
		byte[] b = read(ptr, len);
		LookupPointer returns = null;
		if(len == PTRSHORT){
			returns = LookupPointer.fromBytesShort(b);
			if(returns.equals(LookupPointer.EMPTY_RELATIVE)) returns = null;
		}else if(len == PTR){
			returns = LookupPointer.fromBytes(b);
			if(returns.equals(LookupPointer.EMPTY)) returns = null;
		}else throw new IllegalArgumentException(String.format("Pointer byte is allowed %d, %d", PTR,
			PTRSHORT));

		if(returns != null) returns.withCluster(cluster);
		return returns;
	}

	protected String getString(int ptr, int len){
		byte[] b = read(ptr, len);
		return new String(b, Charset.forName("UTF-8"));
	}

	protected synchronized void setInt(int ptr, int len, int val){
		if(ptr + len >= bodySize) throw new IndexOutOfBoundsException();

		byte[] intb = new byte[len];
		for(int j = len - 1; j >= 0; j--){
			intb[j] = (byte)(val & 0xFF);
			val >>= 8;
		}
		store(intb, ptr);
	}

	protected synchronized void setLong(int ptr, int len, long val){
		if(ptr + len >= bodySize) throw new IndexOutOfBoundsException();

		byte[] longb = new byte[len];
		for(int j = len - 1; j >= 0; j--){
			longb[j] = (byte)(val & 0xFF);
			val >>= 8;
		}
		store(longb, ptr);
	}

	protected synchronized void setPointer(int ptr, int len, LookupPointer val){
		if(ptr + len >= bodySize) throw new IndexOutOfBoundsException();
		byte[] newbyte;
		if(val == null && len == PTR) newbyte = LookupPointer.EMPTY.toBytes();
		else if(val == null && len == PTRSHORT) newbyte = LookupPointer.EMPTY_RELATIVE.toBytesShort();
		else if(len == PTR) newbyte = val.toBytes();
		else if(len == PTRSHORT) newbyte = val.toBytesShort();
		else throw new IllegalArgumentException(
			String.format("Pointer byte is allowed %d, %d", PTR, PTRSHORT));
		store(newbyte, ptr);
	}

	protected synchronized int setString(int ptr, String val){
		byte[] newbyte = val.getBytes(Charset.forName("UTF-8"));
		if(ptr + newbyte.length >= bodySize)
			throw new IndexOutOfBoundsException("From: " + ptr + " + Length: " + newbyte.length
				+ " > bodySize: " + bodySize);
		store(newbyte, ptr);
		return newbyte.length;
	}

	public void setType(int type){
		this.type = type;
	}

	public int getUsed(){
		return this.bodySize - this.bodyVacancy;
	}

	public int getBodyVacancy(){
		return bodyVacancy;
	}

	public void setBodyVacancy(int bodyVacancy){
		this.bodyVacancy = bodyVacancy;
	}

	public void setAboutToFlush(boolean isAboutToFlush){
		this.isAboutToFlush = isAboutToFlush;
	}

	public boolean isAboutToFlush(){
		return isAboutToFlush;
	}

	public ByteBuffer getBorrowedBody(){
		return body;
	}

	public void resetBlockVacancy(){
		this.bodyVacancy = this.bodySize;
	}

	public int getCluster(){
		return cluster;
	}

	public void setCluster(int cluster){
		this.cluster = cluster;
	}
	
	protected byte[] readAllByteArray() {
		return body.array();
	}

	public void readHeader(){
		byte[] values = new byte[INT];
		synchronized(body){
			body.position(0);
			body.get(values); // Header

			body.get(values);
			bodySize = ByteConverter.byteToInt(values);

			body.get(values);
			bodyVacancy = ByteConverter.byteToInt(values);

			body.get(values);
			ID = ByteConverter.byteToInt(values);

			body.get(values);
			type = ByteConverter.byteToInt(values);

			body.get(values);
			cluster = ByteConverter.byteToInt(values);

			body.get(values); // reserved
			body.get(values);

			for(int i = 0; i < 8; i++){
				body.get(values);
				option[i] = ByteConverter.byteToInt(values);
			}
		}
	}

	public void setHeader(){
		synchronized(body){
			body.position(0);
			body.put(HEADER);
			body.put(ByteConverter.intToByte(bodySize, INT));
			body.put(ByteConverter.intToByte(bodyVacancy, INT));
			body.put(ByteConverter.intToByte(ID, INT));
			body.put(ByteConverter.intToByte(type, INT));
			body.put(ByteConverter.intToByte(cluster, INT));
			body.put(ByteConverter.intToByte(0, INT)); // reserved
			body.put(ByteConverter.intToByte(0, INT));
			for(int i = 0; i < 8; i++)
				body.put(ByteConverter.intToByte(option[i], INT));
		}
	}
	
	public void fromRawType(byte[] arrays) {
		body.put(arrays);
		readHeader();
	}

	public ArrayType toArrayType(){
		MessagePackObject[] objectArray = new MessagePackObject[4];
		this.setHeader();
		objectArray[0] = IntegerType.create(ID);
		objectArray[1] = IntegerType.create(cluster);
		objectArray[2] = IntegerType.create(type);
		objectArray[3] = IntegerType.create(0);
		objectArray[4] = RawType.create(body.array());
		
		return ArrayType.create(objectArray);
	}

	public abstract String getClassName();
}